"""
网易云在线音乐下载

分析：
    1、媒体路由中找到音频存放地址
    复制并且进入音频存放地址发现在下载音乐所以这个地址就是我们需要请求的地址
    https://m804.music.126.net/20250116204220/8d824d34e571762756472cc3b110ab81/jdyyaac/obj/w5rDlsOJwrLDjj7CmsOj/18717342062/ef36/ee08/5a14/e6168fd56cac474edf35bdec2a92c9bd.m4a?vuutv=iMn9dKcJuP+9/XSyqRiK3KXT6FzozaRWYmcZl7mh0s5bnXPDXLRH2mBg5/NrB67XlYgW53sZgJm6KU1hI/jSRx5qveqsnYfqQjxbFivBuFo=&authSecret=000001946f0bef0d128b0a30843b2110
    参数：
    vuutv: pFf1RmYnT5Ectpft5RAXPepfsTByjV8f7iYt+zpy90pk9+tDLLlPtb2Hnzgh5jodD5gG0z7pJZL9YHOqyszqGUSVpcGD3Zt4wqxoeF/RLAA=
    authSecret: 000001946f14a0c80c630a30859414e3

    2、搜索参数名称结果
    发现：
        音频地址藏在地址
        https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=736452141d9b8d1b7032c35bc3125ca7
        的response中的url中
        但是要注意的是音频的地址是https协议 url中的是http

        {"data":[{"id":2135146709,"url":"
        http://m704.music.126.net/20250116213337/52dbb944a8d613e7403d553b84a7de36/jdyyaac/obj/w5rDlsOJwrLDjj7CmsOj/34406232085/3939/8d15/d57a/831ad3d255304988df139464e93173cc.m4a?vuutv=XvcYc8P9AtTMwT07saR4E95UQkDj+pA3KRvP3ozPKw6AffDua2chPutuc7cHmrXd1Bmssk2M983MGnKDK/WuKPDDVw6hberwjDDSeENmaNg=&authSecret=000001946f3ae02a1beb0a3283340006","br":96009,"size":2643217,"md5":"831ad3d255304988df139464e93173cc","code":200,"expi":1200,"type":"m4a","gain":0.0,"peak":1.2012,"closedGain":0.0,"closedPeak":0.0,"fee":8,"uf":null,"payed":1,"flag":524292,"canExtend":false,"freeTrialInfo":null,"level":"standard","encodeType":"aac","channelLayout":null,"freeTrialPrivilege":{"resConsumable":false,"userConsumable":false,"listenType":null,"cannotListenReason":null,"playReason":null,"freeLimitTagType":null},"freeTimeTrialPrivilege":{"resConsumable":false,"userConsumable":false,"type":0,"remainTime":0},"urlSource":0,"rightSource":0,"podcastCtrp":null,"effectTypes":null,"time":216750,"message":null,"levelConfuse":null,"musicId":"8951698749"}],"code":200}
        https://m704.music.126.net/20250116213337/52dbb944a8d613e7403d553b84a7de36/jdyyaac/obj/w5rDlsOJwrLDjj7CmsOj/34406232085/3939/8d15/d57a/831ad3d255304988df139464e93173cc.m4a?vuutv=XvcYc8P9AtTMwT07saR4E95UQkDj+pA3KRvP3ozPKw6AffDua2chPutuc7cHmrXd1Bmssk2M983MGnKDK/WuKPDDVw6hberwjDDSeENmaNg=&authSecret=000001946f3ae02a1beb0a3283340006

        参数：
            地址参数：csrf_token: 736452141d9b8d1b7032c35bc3125ca7   这个应该是登录的令牌
            表单参数：
                params: 6j6UpDdbJ/TcKeRpLw7f1EukR8AmqNa69yFph64E38fgLgc5/Q+ifRp+xETcigTCuNtpJF2jSb+W2vVjsZj5JX8Z64zr6BSjd7e3UrPJJtpYzJBVKYjvdlPSBLZE2zMaK9+EqqWNln83/9XazugKJPNh9rxn95L3tCQJVc81QwoIK8N62AonfRv7Lzs0ytWgwaL6ZSl5Gql6YuGiftrtKg==
                encSecKey: 44a980a1b2e997386a26519c3d53dd4e501d9d9141a6abd6e53dc8b8bdccba2db06e62eeb33a331edf19c3777310dee01cb959dd2fb2185bfd43df4737016d7f94739875ef628afb9d398d3ec6a77f83598f459b17255d0280f4ce9b2b4f2784cfa4f554bb5ba8157c2da258e17891b91d6f207a8d40c21e6657dab45d147c4f


    总结：
        所以我们POST请求地址：
            https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=736452141d9b8d1b7032c35bc3125ca7
        就能得到音频地址
        ok现在我们就只要解决如何得到这个地址中的参数params、encSecKey就能成功了！

        1、对比得出
            ①所有音频的藏匿地址都不变  只有参数params、encSecKey改变
            ②搜索路由地址中的enhance/player找到js的文件源文件打开
        2、在源文件中找到js加密方法
            ①请求参数构建的js函数为：
                    ... ... ...
                    bfJ0x = window.asrsea(JSON.stringify(i6c), NQ7J(["怒", "流感"], mM9D.nemj), NQ7J(mM9D.nmd, mM9D.nemj), NQ7J(["可爱", "钻石", "跳舞", "生病"], mM9D.nemj))
            } else {
                bfJ0x = window.asrsea(JSON.stringify(i6c), NQ7J(["流泪", "强"], mM9D.emj), NQ7J(mM9D.md, mM9D.emj), NQ7J(["爱心", "女孩", "惊恐", "大笑"], mM9D.emj))
            }
            e6c.data = j6d.cq7j({
                params: bfJ0x.encText,
                encSecKey: bfJ0x.encSecKey
            })

            window.asrsea = d,
            ②经过阅读js代码知编码的核心方法为：
                 function d(d, e, f, g) {
                    var h = {}
                      , i = a(16);
                    return h.encText = b(d, g),
                    h.encText = b(h.encText, i),
                    h.encSecKey = c(i, e, f),
                    h
                }
                我们只要知道传入的参数d, e, f, g是多少就能得出加密值


            ③对应的
            JSON.stringify(i6c)传给d,
            NQ7J(["怒", "流感"], mM9D.nemj)传给e,
            NQ7J(mM9D.nmd, mM9D.nemj)传给f,
            NQ7J(["可爱", "钻石", "跳舞", "生病"], mM9D.nemj)传给g

            ④我们在
            Ⅰ=》 bfJ0x = window.asrsea(JSON.stringify(i6c), NQ7J(["怒", "流感"], mM9D.nemj), NQ7J(mM9D.nmd, mM9D.nemj), NQ7J(["可爱", "钻石", "跳舞", "生病"], mM9D.nemj))
            } else {
            Ⅱ=》 bfJ0x = window.asrsea(JSON.stringify(i6c), NQ7J(["流泪", "强"], mM9D.emj), NQ7J(mM9D.md, mM9D.emj), NQ7J(["爱心", "女孩", "惊恐", "大笑"], mM9D.emj))
            两处打上断点debugger得知在音乐播放前两个断点中的参数都是一样的并且是直接返回的加密后的参数
            为了得到传入函数的d,e,f,g四个参数我们需要将断点打到函数里面去

            ⑤ 将断点打到函数里后刷新页面  一步一步进行程序 记录每次d,e,f,g参数的变化 找到记录音乐播放前的那一次参数变化
            d = {"ids":"[409941723]","level":"standard","encodeType":"aac","csrf_token":"1bf71d639f97fb901c63b567bb2dfb5e"}
            {"ids":"[509106722]","level":"standard","encodeType":"aac","csrf_token":"38bafc53f6619692319783b5123e1ca9"}
            e = 010001
            f = 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
            g = 0CoJUm6Qyw8W8jud

            最后得到的参数如上 我们知道了e,f,g是固定值  因为e,f,g都是调的NQ7J方法而且方法传的参数都是固定的
            然后d中的参数ids应该是音乐的id后面的都是固定的
            因此我们得出的结论是
            参数：params、encSecKey 就是对音乐的id进行了加密  想办法把播放音乐id的值传给服务器

            ⑥ 接下来就是对核心代码进行拆解 并且用python来实现
                function d(d, e, f, g) {
                    var h = {}      #定义了一个存储对象
                      , i = a(16);  #定义了一个a方法
                    return h.encText = b(d, g), #b方法
                    h.encText = b(h.encText, i),
                    h.encSecKey = c(i, e, f),   #c方法
                    h
                }
                将核心函数中的a,b,c方法进行分析并且用python实现
"""
import random
import string
import base64
import subprocess
from functools import partial

from Crypto.Cipher import DES, AES
from Crypto.Util.Padding import pad, unpad

subprocess.Popen = partial(subprocess.Popen, encoding='utf-8')  # 在导入execjs前加入补丁 解决js编码字符集问题

import execjs
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

from xunter_utils.xunter_requests import ChromeClient


class WY_Music(ChromeClient):
    def __init__(self):
        super().__init__()

    """
    function a(a) {     //分析得出方法a是生成一个16位的随机字符串
        var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
        for (d = 0; a > d; d += 1)
            e = Math.random() * b.length,   //0到1中的随机数乘以b的常数 等于说是对b进行了一次缩放
            e = Math.floor(e),          //取整
            c += b.charAt(e);           //charAt索引取值拼接到c中
        return c
    }

    """

    @staticmethod
    def a_random_str(lens: int = 16) -> str:
        b = string.ascii_letters + string.digits  # 调用string方法生成字符串b
        c = ''
        for d in range(lens):
            e = random.randint(0, len(b) - 1)
            c += b[e]
        return c

    """
        //return CryptoJs.DES.encrypt(text,key,cfg).toString();
        对照得出结论 e为text c为key d为iv初始填充向量0102030405060708
        function b(a, b) {
        var c = CryptoJS.enc.Utf8.parse(b)  //key   对a进行parse转换成utf的字节数据  所以b是进行加密时需要的密钥
          , d = CryptoJS.enc.Utf8.parse("0102030405060708") //这个是iv填充初始向量
          , e = CryptoJS.enc.Utf8.parse(a)  //text  对a进行parse转换成utf的字节数据  所以a是要进行加密的文本
          , f = CryptoJS.AES.encrypt(e, c, {
            iv: d,
            mode: CryptoJS.mode.CBC     //mode为CBC
        });
        return f.toString()
    }
    """

    def b_aes_encrypt(self, a: str, b: str) -> str:
        """
        经过分析js代码得出
        :param a: 进行加密的文本
        :param b: 进行加密时需要的key
        :return:
        """
        iv = '0102030405060708'.encode('utf-8')
        AES_dict = AES.new(key=b.encode('utf-8'), mode=AES.MODE_CBC, iv=iv)  # 创建加密对象
        pad_data = pad(a.encode('utf-8'), AES.block_size)  # 填充字符串数据
        AES_en = AES_dict.encrypt(pad_data)  # 加密
        return base64.b64encode(AES_en).decode()  # base64转换，转字符串

    """
    c方法为RSA加密非对称加密
    很难搞 所以我们一般不用python实现
    而是使用第一种js逆向方式 复制js源码，交给python执行(补充js环境，速度快，难度中等耗时中等)
    这里就要补环境
    function c(a, b, c) {
        var d, e;
        return setMaxDigits(131),
        d = new RSAKeyPair(b,"",c),
        e = encryptedString(d, a)
    }
    我们先找到setMaxDigits方法发现里面又调用了一个方法  像这种方法里面又调方法的我们最好的解决办法就是
    创建一个js文件将与setMaxDigits方法平级的方法都复制粘贴进去并且还要把里面局部的方法改成全局方法
    接着还要将c方法里面调用的方法也都复制粘贴到创建好的js文件中
    """

    def c_rsa(self, a: str, b: str, c: str):
        """
        将js中的c方法和js文件中的js代码拼接后交给python执行
        就实现了c方法
        :param a:
        :param b:
        :param c:
        :return:
        """
        with open(r'./code.js', 'r', encoding='utf-8') as f:
            js_data = f.read()
        js_data += """
        function c(a, b, c) {
            var d, e;
            return setMaxDigits(131),
            d = new RSAKeyPair(b,"",c),
            e = encryptedString(d, a)
    }
        """
        js = execjs.compile(js_data)
        return js.call('c', a, b, c)

    """
    function d(d, e, f, g) {
        var h = {}      #定义了一个存储对象
        , i = a(16);  #定义了一个a方法
        return h.encText = b(d, g), #b方法
        h.encText = b(h.encText, i),
        h.encSecKey = c(i, e, f),   #c方法
        h
    }
    """

    def d_boss(self, ids: int):
        """
        经过分析得出传入的参数中只有id是变化的其他的都是固定值所以只要传一个id进来
        :param ids: 音乐id
        :return:
        """
        # var h = {}  #定义了一个存储对象  , i = a(16);  #定义了一个a方法
        h, i = {}, self.a_random_str()

        d = '{"ids":"[%s]","level":"standard","encodeType":"aac","csrf_token":"38bafc53f6619692319783b5123e1ca9"}' % ids
        e = '010001'
        f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
        g = '0CoJUm6Qyw8W8jud'
        # return h.encText = b(d, g), #b方法
        h['encText'] = self.b_aes_encrypt(d, g)
        # h.encText = b(h.encText, i),
        h['encText'] = self.b_aes_encrypt(h['encText'], i)
        # h.encSecKey = c(i, e, f),  # c方法
        h['encSecKey'] = self.c_rsa(i, e, f)
        return h

    def get_video(self, ids: int):
        bfJ0x = self.d_boss(ids)
        res = self.send_request_post(
            'https://music.163.com/weapi/song/enhance/player/url/v1',
            params = dict(
                csrf_token='38bafc53f6619692319783b5123e1ca9'
            ),
            data = dict(
                params = bfJ0x['encText'],
                encSecKey = bfJ0x['encSecKey'],
            )
        )
        print(res,res.text)
        video_res = self.send_request(res.json()['data'][0]['url'])

        with open(f'{ids}.m4a', 'wb+') as fp:
            fp.write(video_res.content)
        print('下载完毕')

if __name__ == '__main__':
    client = WY_Music()
    client.get_video(1493976789)
